iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 16

Day 16 - 文件載入與分割:用 LangChain 處理外部資料

  • 分享至 

  • xImage
  •  

昨天我們初步認識了 RAG 的基本概念,而在實際應用中,第一步就是將外部資料整理成 AI 能理解與處理的形式。無論是 PDF、Markdown 還是 HTML,這些文件都需要先被載入並分割成適合語言模型處理的片段,之後才能進一步轉換成向量並存入資料庫。

今天我們會深入介紹 LangChain 的 文件載入器(Document Loaders)文本分割器(Text Splitters),並透過程式範例,示範如何把一份長篇文件整理成結構化資料,為後續的檢索與生成做好準備。

為什麼要載入與分割文件?

大型語言模型(LLM)在處理輸入時受限於 上下文長度(Context Length),這個限制決定了模型一次能接收的最大 Token 數量。然而,真實世界的文件往往是成千上萬字的長篇內容,例如數十頁的 PDF、技術規格文件,甚至是完整的書籍。

如果我們直接將整份文件丟進模型,不僅可能超過 Token 上限而被截斷,還會顯著增加推論成本與延遲。更重要的是,長文本通常包含大量與問題無關的資訊,反而會干擾模型的推理,導致回答精準度下降。

因此,在將文件交給模型之前,我們需要對文件進行兩個步驟的預處理:

  1. 載入文件:透過 文件載入器(Document Loaders),將各種不同格式(txt、pdf、docx、Markdown、HTML、網頁內容等)轉換成統一的 Document 物件,並附上 metadata(來源、頁碼、分類等),方便後續檢索與引用。
  2. 切割文本:使用 文本分割器(Text Splitters) 將長篇內容拆分成適當長度的片段(例如 500–1000 tokens),並設定適度的重疊區域(overlap),以確保上下文的連貫性,避免因斷句不佳而遺失語意。

透過這樣的處理流程,我們能在保留語意完整性的同時,將文件轉換成模型可高效處理的格式。這不僅能有效降低 Token 成本,也能讓模型在檢索與回答時維持更高的準確度與上下文一致性。

文件載入器:統一不同來源的資料格式

在 RAG 系統中,第一步就是將原始資料整理成模型可處理的格式。LangChain 提供了 Document Loaders,支援多種常見的檔案格式與資料來源,這些載入器由官方或社群維護,能大幅簡化資料導入流程。

它的核心功能是:將來源資料解析並轉換為統一結構的 Document[] 陣列,每個 Document 物件包含兩部分:

  • pageContent:實際的文字內容。
  • metadata:額外資訊(例如檔案路徑、頁碼、URL、分類標籤等)。

這樣的結構能在後續進行文本分割、向量化與語意檢索時,提供一致且統一的處理方式。

LangChain 提供文件載入器大致可分為兩大類:

  • 檔案載入器:從本地檔案系統讀取資料,例如 TXT、PDF、Word、Markdown 等。完整清單可參考官方文件:File Loaders
  • 網路載入器:從遠端網站或 API 抓取資料,例如 HTML 網頁、RSS Feed、GitHub Repo、Notion Page 等。完整清單可參考官方文件:Web Loaders

以下整理一些常用的載入器與對應套件位置:

文件類型 文件載入器 對應套件路徑
純文字檔 TextLoader langchain/document_loaders/fs/text
PDF 文件 PDFLoader @langchain/community/document_loaders/fs/pdf
HTML 網頁 CheerioWebBaseLoader @langchain/community/document_loaders/web/cheerio
多檔資料夾 DirectoryLoader langchain/document_loaders/fs/directory

Note:部分載入器需要額外安裝第三方套件(例如 PDF 載入器依賴 pdf-parse、HTML 載入器依賴 cheerio)。在使用前,務必確認已安裝相關依賴。

範例:載入純文字檔

適用於 .txt.md 等純文字文件。由於純文字檔沒有複雜結構,載入速度快且相對穩定。

import { TextLoader } from 'langchain/document_loaders/fs/text';

const loader = new TextLoader('data/intro.txt');
const docs = await loader.load();

console.log('文件內容:', docs[0].pageContent);

範例:載入 PDF 文件

需要先安裝 pdf-parse 套件。可選擇一次載入整份 PDF,或按頁分割成多個 Document

import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';

const loader = new PDFLoader('data/annual_report.pdf');
const docs = await loader.load();

console.log('頁數:', docs.length);
console.log('第一頁內容:', docs[0].pageContent);

範例:載入 HTML 網頁

需要先安裝 cheerio 套件。會自動解析 HTML 並擷取純文字內容(例如 <p><h1> 等)。

import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';

const loader = new CheerioWebBaseLoader('https://example.com');
const docs = await loader.load();

console.log('網頁內容:', docs[0].pageContent);

範例:載入資料夾文件

可一次載入資料夾內多個文件,並依副檔名選擇對應的載入器,方便批次處理。

import { DirectoryLoader } from 'langchain/document_loaders/fs/directory';
import { TextLoader } from 'langchain/document_loaders/fs/text';

const loader = new DirectoryLoader('data', {
  '.txt': (path) => new TextLoader(path),
  '.md': (path) => new TextLoader(path),
});
const docs = await loader.load();

console.log(`共載入 ${docs.length} 個 Document`);

在 LangChain 中,無論資料來源是檔案、網頁或 API,最終都會被轉換成 Document[] 陣列。這確保後續的文本分割、向量化與語意檢索都能遵循一致的處理流程。

透過這種抽象化設計,開發者不必擔心資料來源的差異,就能專注於應用邏輯,並在需要時靈活地替換或擴充模組。

文本分割器:將內容分割為可處理片段

在 RAG 或其他需要處理長篇內容的應用中,文件分割(Document Splitting) 幾乎是不可或缺的前處理步驟。它的核心概念是:將長篇文件切割成較小、易於處理的片段(chunks),同時盡量保留必要的上下文資訊。

在實務上,文件分割有以下幾個主要目的:

  • 處理不一致的文件長度:真實世界的文件大小差異極大,分割能確保所有文件都能被一致處理。
  • 克服模型限制:多數嵌入模型與語言模型都有最大輸入長度限制,分割可避免超過上限。
  • 提升表示品質:長文件若一次處理,嵌入向量的語意容易模糊,分割後可讓每段內容更聚焦。
  • 增進檢索精度:較小片段能讓檢索結果更細緻,提升查詢與相關段落的對應度。
  • 優化運算資源:短文本更省記憶體,也更容易平行處理。

在 LangChain 中,Text Splitters 就是專門負責這項任務的工具。它會將 Document[] 中的文字內容切分成多個小片段,並保留原始的 metadata,以便在檢索階段能回溯至原始來源。

常見的切割策略

如何有效地切割長篇內容,是打造高品質 RAG 系統的關鍵之一。根據應用情境與資料型態,可以採用不同的策略,每種方法各有優勢與適用場景。

基於長度(Length-based)

最直觀的方式是依據文字長度進行切割,確保每個片段都不會超過指定大小。這種方法雖然簡單,但在多數情境下都非常實用。其優點包括:

  • 實作容易:規則單純,設定 chunk 大小即可。
  • 片段一致:切割後的長度整齊,方便後續處理。
  • 適應性強:能靈活對應不同模型的輸入限制。

根據計算單位不同,基於長度的切割可分為:

  • Token-based:依 token 數量切割,最適合搭配語言模型,因為能與模型的上下文長度限制直接對應。
  • Character-based:依字元數切割,能在不同類型的文本中保持較一致的效果,常用於一般文字處理場景。

以下為使用 LangChain CharacterTextSplitter 進行字元切割的範例:

import { CharacterTextSplitter } from "@langchain/textsplitters";

const textSplitter = new CharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 0,
});
const texts = await textSplitter.splitText(document);

這段程式會將輸入的 document 依字元數切分為多個小片段,確保每段長度不超過上限。

  • chunkSize:每個片段的最大長度(此例為 100 個字元)。
  • chunkOverlap:片段間的重疊字元數(此例為 0,表示不重疊)。

Tip:在 RAG 應用中,通常會設定 chunkOverlap(例如 50–100 tokens),以保留跨段落的上下文,避免語意斷裂。

基於文本結構(Text-structured based)

自然語言文本本身具備層級結構,例如 段落 → 句子 → 單詞。在切割文件時,沿用這些語法與語意層級,可以保留語句的流暢性與語意完整性,避免像單純依長度切割時,可能出現斷句不自然或語意中斷的問題。

LangChain 提供的 RecursiveCharacterTextSplitter 正是這種策略的代表性實作。它的核心運作邏輯如下:

  • 優先依段落切割:若段落長度超過設定,才進行下一層處理。
  • 再依句子切割:若句子仍過長,則繼續細分。
  • 最後依單詞切割:確保不會出現超過 chunkSize 的片段。

使用範例如下:

import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 0,
});
const texts = await textSplitter.splitText(document);

這種遞迴式的切割方式,能讓片段在語意上盡可能保持完整,同時確保每段內容符合模型的輸入長度要求,是處理長篇自然語言文本時的常用選擇。

基於文件結構(Document-structured based)

有些文件天生就具備明確的層次結構,例如 HTML 網頁、Markdown 文件、JSON 資料或程式碼檔案。這些格式通常已經依章節、段落、欄位或功能區塊進行自然分組,因此我們可以直接利用這些結構來切割文件,而不必完全依賴長度或語意分析。

基於文件結構的切割主要有以下優點:

  • 保留文件邏輯組織:避免將屬於同一主題的內容拆散,有助於維持原有語意脈絡。
  • 維持上下文一致性:每個片段的內容彼此高度相關,減少檢索時的不相關匹配。
  • 適合結構化任務:例如知識庫檢索、API 文件解析、程式碼輔助等,結構切割可直接對應應用場景。

常見應用範例:

  • Markdown:依標題層級(#、##、###)切割,確保每個片段對應到一個章節或小節。
  • HTML:依主要標籤(如 、、)切割,保持文章或內容的完整性。
  • JSON:依物件(object)或陣列元素(array element)切割,方便針對特定資料區塊檢索。
  • 程式碼:依函式、類別或邏輯區塊(如 if-else)切割,便於後續的程式碼理解或自動化重構。

需要注意的是,文件結構可能存在多層嵌套或混合格式,例如 HTML 內嵌 Markdown。這時候切割邏輯必須能同時處理不同層級的解析,才能正確保留內容的階層關係。此外,若單一結構單位本身過於龐大,仍建議搭配 基於長度 的切割策略進一步細分,以避免超出模型輸入限制,並確保後續處理的效率與穩定性。

基於語意的分割(Semantic meaning based)

與前面依長度或結構切割的方式不同,基於語意的分割並不是單純依照字數或標點符號來決定斷點,而是直接分析文本的語意內容,找出自然的轉折點。它的核心理念是:當主題或語意出現明顯變化時,才進行切割,以確保每個片段在語意上完整且連貫。

這種方法的典型實作流程通常依賴 Embedding 相似度分析

  1. 選取片段:先從文件中擷取前幾句或一小段文字,並生成一個 Embedding 向量,作為該片段的語意表示。
  2. 滑動視窗:持續往後移動,對下一段文字生成 Embedding。
  3. 比較相似度:計算兩個相鄰 Embedding 之間的語意相似度。
  4. 判斷分割點:若相似度低於設定的閾值,表示語意已經產生明顯轉換,則在此位置切割。

這種方法的最大優勢是能夠保證每個片段在語意上的一致性與完整性,對於 語意檢索、文件摘要、問答系統 等應用的精準度有顯著提升。不過,它的缺點也很明顯:需要將大量文字反覆轉換成 Embedding,並進行多次相似度比較,因此計算成本高,實作邏輯也比單純依長度或結構切割更複雜。

Note:所謂 Embedding,就是把一段文字轉換成一個多維向量,用數字的方式表示該文字的語意特徵。這樣不同的文字就能透過向量之間的距離或相似度來比較語意上的接近程度。後續內容我們會更深入介紹 Embedding 的概念與用法。

小結

今天我們學會了如何用 LangChain 的 文件載入器(Document Loaders)文本分割器(Text Splitters),把外部資料整理成 AI 能有效處理的格式:

  • 文件載入與分割的目的,是將不同來源的內容統一格式,克服上下文長度限制,並提升檢索與生成的效率與準確度。
  • 文件載入器能將各種來源(txt、pdf、md、HTML、網頁、資料夾)轉換成統一的 Document[],並附帶 metadata 以便後續追蹤。
  • 常見載入器包含 TextLoaderPDFLoaderCheerioWebBaseLoaderDirectoryLoader,部分需要額外安裝第三方套件。
  • 文本分割器能將長文件分成小片段(chunks),避免超過模型的上下文限制,提升檢索精度與運算效率。
  • 基於長度的分割:依字元或 Token 數分割,簡單實用。
  • 基於文本結構的分割:利用段落、句子、單詞等自然層級,保留語意流暢度。
  • 基於文件結構的分割:依 HTML 標籤、Markdown 標題、JSON 物件或程式碼區塊切割,維持邏輯組織。
  • 基於語意的分割:透過 Embedding 相似度偵測主題轉折,確保語意完整,但計算成本較高。

文件載入與分割雖然只是 RAG 流程的第一步,但它決定了後續檢索與生成的品質,是打造可靠 AI 應用的基礎。


上一篇
Day 15 - 認識 RAG:檢索增強生成初探
下一篇
Day 17 - 嵌入模型與向量資料庫:建構可語意檢索的 AI 知識庫
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言